50% off your first hosting order, applied in your cart, on our upgraded infrastructure! Ends in: View Plans →
Türkiye · France · Northern Cyprus05372609483
Cloud VDS

Docker on a VDS: Installing, Composing and Publishing Securely on Ubuntu 24.04

  • 2 November 2026
  • 4 min read
Contents
    Share this post

    Docker is the most practical way to package an application with its dependencies and run several of them side by side on one server. It does not run on shared hosting; because it needs root, a Türkiye VDS or France VDS is the right base. This guide goes from a clean Ubuntu 24.04 LTS install to an application in production.

    Which VDS plan?

    • One small application (n8n, Uptime Kuma, a small API): 2 cores, 2 GB RAM and 35 GB NVMe are enough.
    • Application + database + reverse proxy: 4 cores and 4 GB RAM run comfortably; NVMe storage makes a difference for the database.
    • Several projects or a CI runner: 8 cores, 8 GB RAM and up. The France plans with AMD EPYC 4584PX processors and DDR5 memory are faster for build-heavy work.

    Take a snapshot in the FiberVM panel before you start; if anything goes wrong you are back in a minute. You can also reinstall the server with Ubuntu 24.04 from the same panel.

    1. Prepare the server

    apt update && apt upgrade -y
    adduser deploy && usermod -aG sudo deploy
    timedatectl set-timezone Europe/Istanbul

    For SSH keys, disabling password login and UFW, follow our first-setup security checklist. Open only ports 22, 80 and 443 in UFW for now.

    2. Install Docker from the official repository

    Ubuntu's own docker.io package lags behind; use Docker's repository:

    sudo apt install -y ca-certificates curl
    sudo install -m 0755 -d /etc/apt/keyrings
    sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
    sudo chmod a+r /etc/apt/keyrings/docker.asc
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] 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 update
    sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    sudo usermod -aG docker deploy

    Log out and back in, then verify with docker run --rm hello-world. A user in the docker group effectively has root; add only accounts you trust.

    3. Log limits and restart policy

    By default container logs grow without limit and fill the disk. Put this in /etc/docker/daemon.json and run systemctl restart docker:

    {
      "log-driver": "json-file",
      "log-opts": { "max-size": "20m", "max-file": "5" }
    }

    Add restart: unless-stopped to every service in your Compose files so the application comes back on its own after a reboot.

    4. Reverse proxy with SSL: a Caddy example

    Instead of binding applications to 80/443 directly, put a reverse proxy in front. Caddy obtains and renews Let's Encrypt certificates itself. Example compose.yaml:

    services:
      caddy:
        image: caddy:2
        restart: unless-stopped
        ports: ["80:80", "443:443"]
        volumes:
          - ./Caddyfile:/etc/caddy/Caddyfile
          - caddy_data:/data
      app:
        image: ghcr.io/example/app:latest
        restart: unless-stopped
        environment:
          - DATABASE_URL=postgres://app:secret@db:5432/app
        depends_on: [db]
      db:
        image: postgres:16
        restart: unless-stopped
        environment:
          - POSTGRES_USER=app
          - POSTGRES_PASSWORD=secret
          - POSTGRES_DB=app
        volumes:
          - db_data:/var/lib/postgresql/data
    volumes:
      caddy_data:
      db_data:

    The Caddyfile is two lines: app.example.com { reverse_proxy app:3000 }. Point the domain's A record at the VDS IP, run docker compose up -d, and the certificate is issued on the first request. The database and the application publish no ports, so they are reachable only through Caddy.

    5. The UFW trap: Docker bypasses your rules

    When Docker sees a published port such as ports: "5432:5432" it writes its own iptables rules and bypasses the UFW block. A database port you believe is closed may be open to the internet. Two safe approaches:

    • Do not write a ports line for services that need no outside access; containers on the same Compose network reach each other by name.
    • If you need local access, bind the port to loopback only: "127.0.0.1:5432:5432", then connect through an SSH tunnel.

    Verify from another machine with nmap -p 1-65535 VDS_IP; only 22, 80 and 443 should show as open.

    6. Updates and backups

    • Image updates: docker compose pull && docker compose up -d; remove old images with docker image prune -f.
    • Data backups: dump the database from inside the container: docker compose exec db pg_dump -U app app | gzip > backup-$(date +%F).sql.gz. Copying volume directories (/var/lib/docker/volumes) while running produces inconsistent backups; prefer dumps.
    • Off-server copy: send backups to a VDS in another location or to object storage. A snapshot alone is not a backup; see our VDS backup strategy article.
    • Monitoring: the live CPU, RAM and disk graphs in the FiberVM panel show the combined load of your containers; use docker stats per service.

    If you are unsure which server fits, open a support ticket with the applications you plan to run and we will pick the plan together. Unlimited traffic and DDoS protection are included on every VDS plan.

    Sources

    Comments0

    No Comments Yet

    Be the first to share your thoughts.

    Join the Discussion

    Please sign in or sign up to leave a comment.

    Never Miss a Post

    Get our latest tutorials, guides and product news in your inbox.

    You're subscribed to our newsletter.