Self-hosting Strapi on a Hetzner Ubuntu Server

Self-hosting Strapi on a Hetzner Ubuntu Server

Yulei Chen - Content-Engineerin bei sliplane.ioYulei Chen
11 min

Strapi is a popular open-source headless CMS that lets you build powerful APIs with a built-in content management interface. By self-hosting Strapi on your own Ubuntu server, you get full control over your data, no vendor lock-in, and predictable hosting costs.

Follow along this easy-to-understand guide to learn how you can deploy your own Strapi instance using Docker and Caddy web server for automatic HTTPS.

For this post, we're using an affordable server from Hetzner. Hetzner is known to provide great service at an exceptional price/performance ratio, making it an excellent choice for hosting Strapi.

Prerequisites

Before we start, make sure you have a Hetzner Cloud account (or be ready to create one).

Step 1: Setup Your Hetzner Server

If you don't have a Hetzner server yet, follow these steps to create one:

  1. Go to the Hetzner Cloud Console, choose a project or create a new one, then navigate to ServersAdd Server

Hetzner Cloud Console

  1. Follow Hetzner's guidelines to choose:
    • Server type: Select a server type that fits your needs. For Strapi with PostgreSQL, at least 2 GB RAM is recommended.

Select Server Type

  • Location: Choose a data center location closest to you or your users.

Select Location

  • Image: Select Ubuntu (latest LTS version recommended).

Select Ubuntu Image

  1. Add SSH key: Add your SSH public key for secure access. If you don't have an SSH key yet, you can generate one using ssh-keygen:
Terminal
ssh-keygen -t ed25519 -C "your_email@example.com"

Check it out with cat ~/.ssh/id_ed25519.pub and paste it into your server.

SSH Connection

  1. Configure networking if needed, then click Create & Pay to provision your server

Networking Options

Once your server is created, note down its IP address. You'll use this to connect via SSH in the next step.

Server IP Address

Step 2: Update Your Server

Open your terminal and log into your Ubuntu server via SSH:

Terminal
ssh root@your-server-ip

and update the system to ensure it has the latest security patches and updates:

Terminal
sudo apt-get update
sudo apt-get upgrade -y

Once finished, your server is ready for installing the software.

Step 3: Install and Configure UFW Firewall

Only keep necessary ports open: SSH (22), HTTP (80), HTTPS (443).

Install UFW and configure the firewall as follows:

Terminal
sudo apt install ufw -y
sudo ufw allow 22    # SSH
sudo ufw allow 80    # HTTP
sudo ufw allow 443   # HTTPS
sudo ufw enable

Check your firewall configuration:

Terminal
sudo ufw status verbose

Docker can sometimes ignore UFW rules. To tackle this, verify extra settings as explained here.

Step 4: Docker Installation

Docker will run Strapi and PostgreSQL in containers. Install Docker by running these commands:

Setup dependencies and Docker's GPG key:

Terminal
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

Add Docker repository:

Terminal
echo \
  "deb [arch=$(dpkg --print-architecture) \
signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo $VERSION_CODENAME) stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update

Install Docker Engine and compose-plugin:

Terminal
sudo apt-get install docker-ce docker-ce-cli \
containerd.io docker-buildx-plugin docker-compose-plugin -y

Check installation:

Terminal
sudo docker run hello-world

If you see the "hello-world" message, Docker is ready.

Step 5: Install Caddy for Automatic HTTPS

Caddy simplifies HTTPS configuration since it handles SSL certificates automatically from Let's Encrypt.

Install Caddy:

Terminal
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list

sudo apt update
sudo apt install caddy -y

Before configuring Caddy, you need to point your domain to your server's IP address. If you haven't configured DNS yet, follow these steps:

Configure DNS for Your Domain

  1. Log into your domain registrar's dashboard (where you purchased your domain)
  2. Navigate to the DNS settings or DNS management section
  3. Add an A record with the following settings:
    • Type: A
    • Name: @ (for root domain) or a subdomain like strapi (for strapi.yourdomain.com)
    • Value/Target: Your Hetzner server's IPv4 address
  4. Add an AAAA record for IPv6 support:
    • Type: AAAA
    • Name: @ (for root domain) or the same subdomain you used for the A record
    • Value/Target: Your Hetzner server's IPv6 address

DNS changes can take a few minutes to several hours to propagate. You can check if your DNS is configured correctly using tools like dig or online DNS checkers. Once the DNS record is active, you can proceed with Caddy configuration.

Configure Caddy

Edit the Caddyfile configuration file:

Terminal
sudo nano /etc/caddy/Caddyfile

Enter your domain and configure reverse proxy. Replace "yourdomain.com" with your actual domain name:

Caddyfile
yourdomain.com {
    reverse_proxy localhost:1337
}

If no domain yet, use this temporarily:

Caddyfile
:80 {
    reverse_proxy localhost:1337
}

Restart Caddy to load the config:

Terminal
sudo systemctl restart caddy

Step 6: Run Strapi with Docker Compose

We're going to use Docker Compose for easier setup. Strapi needs a PostgreSQL database, so the compose file includes both services.

First, create a directory for Strapi, then navigate to it and create the compose file:

Terminal
mkdir -p ~/strapi
cd ~/strapi
sudo nano compose.yml

Before adding the compose file, generate secure secrets for your Strapi instance:

Terminal
openssl rand -base64 32

Run this command four times and save the outputs — you'll use them for JWT_SECRET, ADMIN_JWT_SECRET, and APP_KEYS below.

Copy/paste the following content into compose.yml, replacing the placeholder secrets and passwords with your generated values:

compose.yml
services:
  strapi:
    image: elestio/strapi-production:v5.33.4
    entrypoint: /bin/sh
    command:
      - -c
      - |
        cat > /opt/app/src/admin/vite.config.js << 'EOFVITE'
        const { mergeConfig } = require('vite');
        module.exports = (config) => {
          return mergeConfig(config, {
            server: {
              allowedHosts: true
            }
          });
        };
        EOFVITE
        exec npm run develop
    restart: unless-stopped
      - "127.0.0.1:1337:1337"
    environment:
      DATABASE_CLIENT: postgres
      DATABASE_HOST: postgresql
      DATABASE_PORT: "5432"
      DATABASE_NAME: strapi
      DATABASE_USERNAME: strapi
      DATABASE_PASSWORD: replace-with-secure-db-password
      JWT_SECRET: replace-with-generated-secret-1
      ADMIN_JWT_SECRET: replace-with-generated-secret-2
      APP_KEYS: replace-with-generated-secret-3,replace-with-generated-secret-4
      NODE_ENV: development
      STRAPI_TELEMETRY_DISABLED: "true"
    volumes:
      - strapi-config:/opt/app/config
      - strapi-src:/opt/app/src
      - strapi-uploads:/opt/app/public/uploads
    depends_on:
      postgresql:
        condition: service_healthy

  postgresql:
    image: postgres:16.4
    restart: unless-stopped
    environment:
      POSTGRES_DB: strapi
      POSTGRES_USER: strapi
      POSTGRES_PASSWORD: replace-with-secure-db-password
      PGDATA: /var/lib/postgresql/data
    volumes:
      - strapi-postgresql-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U strapi -d strapi"]
      interval: 5s
      timeout: 20s
      retries: 10

volumes:
  strapi-config:
  strapi-src:
  strapi-uploads:
  strapi-postgresql-data:

The image versions above were current at time of writing. Check Docker Hub for the latest stable Strapi version and Docker Hub for PostgreSQL.

Make sure DATABASE_PASSWORD matches POSTGRES_PASSWORD — they need to be the same value. For more tips on running PostgreSQL in containers, check out our guide on best practices for Postgres in Docker.

Start Strapi:

Terminal
sudo docker compose up -d

Docker pulls the Strapi and PostgreSQL images and runs them in the background. The first startup may take a minute or two while Strapi builds the admin panel.

You can watch the logs to see when it's ready:

Terminal
sudo docker compose logs -f strapi

Wait until you see a message like Strapi started or Admin panel ready, then press Ctrl+C to exit the logs.

Step 7: Access Your Self-Hosted Strapi Instance

Your Strapi instance should now load successfully at https://yourdomain.com. If you're using a temporary HTTP setup, open http://your-server-ip:1337.

On the first visit, Strapi will ask you to create an admin account. Fill in your name, email, and a strong password to set up the initial administrator user.

Once logged in, you'll see the Strapi admin panel where you can start building your content types and APIs.

Security Recommendations

Public servers should always be secure. The following practices are recommended:

  • Set strong passwords: Use unique, complex passwords for both your Strapi admin account and PostgreSQL database.
  • HTTPS is already enabled: Caddy handles this automatically via Let's Encrypt.
  • Keep everything updated: Regularly apply system updates, Docker image updates, and Strapi updates.
  • Review firewall rules: Periodically check your UFW configuration and ensure only necessary ports are open.
  • Back up your data: Set up regular backups for your PostgreSQL database and uploaded files. See our guide on backing up and restoring Postgres via SSH tunnel for more details.
  • Monitor logs: Watch server and container logs for suspicious activity. Install tools like Fail2ban for extra security.

Updating Your Strapi Installation

When you want to update your Strapi instance, first check the latest version on Docker Hub, then update the image version in your compose.yml file and run:

Terminal
cd ~/strapi
docker compose pull
docker compose up -d

Docker will download the updated images and replace your current containers. Your data is safe in the named volumes.

Cost Comparison with Other Providers

Self-hosting Strapi typically results in lower costs compared to managed hosting:

ProvidervCPU CoresRAMDiskEstimated Monthly CostNotes
Sliplane22 GB40 GB€9charge per server
Render12 GB40 GB~$35–$45VM Small
Fly.io22 GB40 GB~$20–$25VM + volume
Railway22 GB40 GB~$15–$66Usage-based

Conclusion

You now have a self-hosted Strapi instance running on Hetzner with Docker, PostgreSQL, and automatic HTTPS via Caddy. This setup gives you full control over your headless CMS at a fraction of the cost of managed services. If you're also looking at other headless CMS options, check out our tutorials on self-hosting Directus or running Payload CMS in Docker.

Deploy Strapi in minutes

Skip server setup and run Strapi on Sliplane with one click, HTTPS, and persistent storage.