# VM Setup # 0) One-time VM prep (as root just this once) SSH to the VM your provider gave you (often only root works initially): ```bash ssh root@ ``` Create a non-root deploy user with sudo, and lock down SSH: ```bash # create user adduser deploy usermod -aG sudo deploy # add your SSH key mkdir -p /home/deploy/.ssh chmod 700 /home/deploy/.ssh nano /home/deploy/.ssh/authorized_keys # paste your public key chmod 600 /home/deploy/.ssh/authorized_keys chown -R deploy:deploy /home/deploy/.ssh # harden SSH (optional but recommended) sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config systemctl reload sshd exit ``` Now reconnect as your non-root user: ```bash ssh deploy@ ``` # 1) Firewall and basics ```bash # Ubuntu/Debian sudo apt update sudo apt install -y ufw # allow SSH + web sudo ufw allow OpenSSH sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable sudo ufw status ``` # 2) Install Docker Engine + Compose plugin (non-root usage) ```bash # Docker official repo sudo apt-get install -y 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 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 sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # let your user run docker without sudo sudo usermod -aG docker $USER newgrp docker # optional: limit container logs echo '{"log-driver":"json-file","log-opts":{"max-size":"10m","max-file":"3"}}' | \ sudo tee /etc/docker/daemon.json sudo systemctl restart docker ``` # 3) Layout for your Compose stacks We’ll keep everything under `/opt/compose`, owned by `deploy`: ```bash sudo mkdir -p /opt/compose/{traefik,portainer,gitea,authentik} sudo chown -R deploy:deploy /opt/compose ``` Create the shared external Docker network (once): ```bash docker network create proxy ``` # 4) Copy your compose files (no root, via scp/rsync) From your **local** machine: ```bash # example: copy a whole folder into /opt/compose/portainer scp -r ./portainer/* deploy@:/opt/compose/portainer/ # or use rsync (recommended) rsync -avz ./gitea/ deploy@:/opt/compose/gitea/ ``` # 5) Traefik on the VM (HTTP-01 with Let’s Encrypt) On the VM: ```bash cd /opt/compose/traefik ``` Create `compose.yml`: ```yaml version: "3.9" services: traefik: image: traefik:v3.1 restart: unless-stopped command: - --providers.docker=true - --providers.docker.exposedByDefault=false - --entrypoints.web.address=:80 - --entrypoints.websecure.address=:443 - --entrypoints.web.http.redirections.entryPoint.to=websecure - --entrypoints.web.http.redirections.entryPoint.scheme=https # Let's Encrypt (HTTP-01 challenge) - --certificatesresolvers.le.acme.email=${LE_EMAIL} - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json - --certificatesresolvers.le.acme.httpchallenge=true - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web # Optional dashboard (protect later) - --api.dashboard=true ports: - "80:80" - "443:443" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./letsencrypt:/letsencrypt networks: - proxy labels: - traefik.enable=true - traefik.http.routers.traefik.rule=Host(`traefik.YOURDOMAIN.com`) - traefik.http.routers.traefik.entrypoints=websecure - traefik.http.routers.traefik.tls.certresolver=le - traefik.http.routers.traefik.service=api@internal networks: proxy: external: true ``` Create the storage file and set strict perms: ```bash mkdir -p /opt/compose/traefik/letsencrypt touch /opt/compose/traefik/letsencrypt/acme.json chmod 600 /opt/compose/traefik/letsencrypt/acme.json ``` Create `.env`: ```bash echo "LE_EMAIL=you@example.com" > /opt/compose/traefik/.env ``` Bring it up: ```bash cd /opt/compose/traefik docker compose up -d ``` # 6) DNS records on GoDaddy Point your domain/subdomains to the VM’s **public IP**: - `A @ -> ` - `A traefik -> ` - `A portainer -> ` - `A git -> ` - `A auth -> ` (HTTP-01 will fetch per-host certs automatically the first time you visit each hostname.) > If you want a **wildcard** (`*.example.com`), switch Traefik to **DNS-01** with your DNS provider’s API. GoDaddy’s API can be restrictive; moving DNS hosting to Cloudflare is common. But HTTP-01 works fine for named subdomains. # 7) Example app stacks (all non-root) ## Portainer (behind Traefik) `/opt/compose/portainer/compose.yml` ```yaml version: "3.9" services: portainer: image: portainer/portainer-ce:latest restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock - portainer_data:/data networks: - proxy labels: - traefik.enable=true - traefik.http.routers.portainer.rule=Host(`portainer.YOURDOMAIN.com`) - traefik.http.routers.portainer.entrypoints=websecure - traefik.http.routers.portainer.tls.certresolver=le - traefik.http.services.portainer.loadbalancer.server.port=9000 volumes: portainer_data: networks: proxy: external: true ``` Deploy: ```bash cd /opt/compose/portainer docker compose up -d ``` ## Gitea (behind Traefik) `/opt/compose/gitea/compose.yml` ```yaml version: "3.9" services: gitea: image: gitea/gitea:1 restart: unless-stopped environment: - USER_UID=1000 - USER_GID=1000 volumes: - gitea_data:/data networks: - proxy labels: - traefik.enable=true - traefik.http.routers.gitea.rule=Host(`git.YOURDOMAIN.com`) - traefik.http.routers.gitea.entrypoints=websecure - traefik.http.routers.gitea.tls.certresolver=le - traefik.http.services.gitea.loadbalancer.server.port=3000 volumes: gitea_data: networks: proxy: external: true ``` (Do the same for Authentik; keep it on `proxy` and add Traefik labels to the web service.) # 8) Secure the Traefik dashboard (quick basic-auth) Create a middleware once and attach it to the dashboard router. Generate a bcrypt hash (on your laptop): ```bash # Install apache2-utils if you have it, or use Docker to generate: docker run --rm httpd:2.4-alpine htpasswd -nbB admin 'YOUR_STRONG_PASSWORD' # Output looks like: admin:$2y$05$.... ``` Add to Traefik labels: ```yaml labels: - traefik.enable=true - traefik.http.middlewares.basicauth.basicauth.users=admin:$$2y$$05$$ - traefik.http.routers.traefik.rule=Host(`traefik.YOURDOMAIN.com`) - traefik.http.routers.traefik.entrypoints=websecure - traefik.http.routers.traefik.tls.certresolver=le - traefik.http.routers.traefik.middlewares=basicauth@docker - traefik.http.routers.traefik.service=api@internal ``` Then: ```bash cd /opt/compose/traefik && docker compose up -d ``` # 9) Quality-of-life tips - Containers should include `restart: unless-stopped`; Docker will auto-start them on reboot—no systemd unit needed. - Keep everything on the `proxy` network; only Traefik publishes 80/443 to the host. - For updates: `docker compose pull && docker compose up -d` per stack. - Backups: snapshot `/opt/compose/*` and any named volumes (`/var/lib/docker/volumes/...`), or mount volumes to known paths you can back up. --- If you want, paste your existing Traefik/Authentik/Gitea labels here and I’ll adapt them for the VM layout (and wire Authentik as forward-auth to protect Portainer/Gitea).