How to Do a Docker Setup for Small Projects

by David Park
How to Do a Docker Setup for Small Projects

This guide walks you through a production-ready Docker setup for small projects on Ubuntu 24.04. By the end you'll have Docker Engine installed, a working docker-compose.yml for a typical web app with a database, a non-root user that can run containers, and a systemd-managed service that survives reboots — all without paying for Docker Desktop or a managed container platform.

Prerequisites

  • Ubuntu 24.04 LTS server or workstation (fresh install or existing)
  • A user account with sudo privileges
  • Outbound internet access to reach download.docker.com
  • Basic familiarity with the terminal and a text editor (nano or vim)
  • Ports 80 and 443 free if you plan to expose a web app

Step 1 — Remove Conflicting Packages

Ubuntu 24.04 ships with stub Docker packages that conflict with the official Engine. Remove them first.

sudo apt-get remove -y docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc

Expected output: each package is listed as "not installed" or is removed without error. It is safe to run even on a clean system.

Step 2 — Add the Official Docker APT Repository

2a. Install the packages needed to add an HTTPS APT source.

sudo apt-get update
sudo apt-get install -y ca-certificates curl

2b. Download and store Docker's GPG key.

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

2c. Add the repository to your sources list.

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

This writes a single-line .list file pointing to the noble (24.04) stable channel.

Step 3 — Install Docker Engine and the Compose Plugin

Update the package index now that the new repo is registered, then install.

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

Expected output ends with:

Setting up docker-ce (5:26.x.x-1~ubuntu.24.04~noble) ...

The docker-compose-plugin package provides the docker compose subcommand (V2). There is no separate docker-compose binary to manage.

Step 4 — Allow Your User to Run Docker Without sudo

Adding your user to the docker group avoids prefixing every command with sudo.

sudo usermod -aG docker $USER

Log out and back in (or run newgrp docker in the current shell) for the group membership to take effect.

newgrp docker

Security note: Members of the docker group have effective root access to the host. On a shared server, use sudo docker instead and skip this step.

Step 5 — Enable Docker to Start on Boot

Enable and start the systemd units for Docker and containerd.

sudo systemctl enable --now docker containerd

Expected output:

Synchronizing state of docker.service with SysV service script with /usr/lib/systemd/systemd-sysv-install.
Executed /usr/lib/systemd/systemd-sysv-install enable docker

Step 6 — Create a Project Directory and a docker-compose.yml

This example sets up a small Node.js web app backed by PostgreSQL 16 — a common pattern for indie projects. Adjust the image names and ports to match your stack.

6a. Create the project directory.

mkdir -p ~/projects/myapp && cd ~/projects/myapp

6b. Create the Compose file.

nano docker-compose.yml

Paste the following content:

services:
  web:
    image: node:20-alpine
    working_dir: /app
    volumes:
      - ./app:/app
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://appuser:changeme@db:5432/appdb
    depends_on:
      db:
        condition: service_healthy
    command: ["node", "server.js"]
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: changeme
      POSTGRES_DB: appdb
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  db_data:

Key decisions explained:

  • node:20-alpine and postgres:16-alpine use Alpine-based images — smaller pull size, lower attack surface.
  • db_data is a named volume so database files survive docker compose down.
  • depends_on with service_healthy prevents the web container from starting before PostgreSQL is ready to accept connections.
  • restart: unless-stopped means containers come back after a server reboot without a separate systemd unit.

6c. Create a minimal placeholder app so the web container has something to run.

mkdir -p app
cat > app/server.js << 'EOF'
const http = require('http');
http.createServer((req, res) => {
  res.writeHead(200);
  res.end('OK\n');
}).listen(3000, () => console.log('Listening on 3000'));
EOF

Step 7 — Set Secrets in an .env File (Not in the Compose File)

Hardcoding credentials in docker-compose.yml is fine for local dev, but even for small projects you should use an .env file and exclude it from version control.

7a. Create the .env file.

cat > .env << 'EOF'
POSTGRES_USER=appuser
POSTGRES_PASSWORD=changeme
POSTGRES_DB=appdb
EOF

7b. Update docker-compose.yml to reference the variables.

Replace the hardcoded environment block under db with:

    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}

And update the DATABASE_URL under web:

      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}

7c. Add .env to .gitignore.

echo ".env" >> .gitignore

Docker Compose automatically reads .env from the same directory as the Compose file, so no extra flags are needed.

Step 8 — Pull Images and Start the Stack

Pull all images declared in the Compose file before starting, so you can catch network issues separately from startup issues.

docker compose pull

Then start the stack in detached mode.

docker compose up -d

Expected output:

[+] Running 3/3
 ✔ Network myapp_default  Created
 ✔ Container myapp-db-1   Started
 ✔ Container myapp-web-1  Started

Verify It Works

Check container status. Both containers should show Up and db should show (healthy).

docker compose ps

Expected output:

NAME            IMAGE              COMMAND                  SERVICE   STATUS          PORTS
myapp-db-1      postgres:16-alpine "docker-entrypoint.s…"   db        Up (healthy)    5432/tcp
myapp-web-1     node:20-alpine     "node server.js"         web       Up              0.0.0.0:3000->3000/tcp

Hit the web endpoint.

curl -s http://localhost:3000

Expected output:

OK

Confirm PostgreSQL is reachable from the web container.

docker compose exec web sh -c "apk add --no-cache postgresql-client -q && psql \"$DATABASE_URL\" -c 'SELECT 1;'"

Expected output includes:

 ?column?
----------
        1
(1 row)

Confirm data persists after a restart.

docker compose down && docker compose up -d
curl -s http://localhost:3000

The db_data volume retains PostgreSQL data across restarts.


Troubleshooting

Got permission denied while trying to connect to the Docker daemon socket You have not logged out after adding yourself to the docker group. Run newgrp docker or open a new terminal session.

port is already allocated on port 3000 or 5432 Another process is using that port. Find it with sudo ss -tlnp | grep 3000 and stop it, or change the host-side port in docker-compose.yml (e.g., "3001:3000").

myapp-web-1 exits immediately The app crashed on startup. Inspect logs with docker compose logs web. Common cause: server.js missing or syntax error in the file.

db container stuck in (health: starting) for more than 60 seconds Check logs with docker compose logs db. Likely causes: volume permission issue or the POSTGRES_PASSWORD variable is empty. Verify .env is present and non-empty.

Images fail to pull behind a corporate proxy Set HTTP_PROXY and HTTPS_PROXY in /etc/systemd/system/docker.service.d/proxy.conf and run sudo systemctl daemon-reload && sudo systemctl restart docker.

docker compose command not found You installed the legacy standalone docker-compose instead of the plugin. Verify with docker compose version. If missing, re-run Step 3 and confirm docker-compose-plugin is listed in apt-get install.


Next Steps

With your Docker setup for small projects running, consider these additions:

  • Add a reverse proxy. Drop an nginx:alpine or caddy:alpine service into the same Compose file to handle TLS termination and route traffic from port 443 to your app container.
  • Set up automated backups for the named volume. Run docker run --rm -v myapp_db_data:/data -v $(pwd):/backup alpine tar czf /backup/db_backup.tar.gz /data on a cron schedule.
  • Pin image digests for reproducibility. Replace postgres:16-alpine with postgres:16-alpine@sha256:<digest> to prevent silent image updates from breaking your app.
  • Use Docker's BuildKit for custom images. When you outgrow off-the-shelf images, add a build: . key to your service and a Dockerfile in the project root. BuildKit is enabled by default in Docker Engine 23+.
  • Monitor container resource usage. Run docker stats for a live view of CPU and memory per container — useful for right-sizing your VPS before you scale.