<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Edwin Sanjo Soji</title>
    <description>The latest articles on DEV Community by Edwin Sanjo Soji (@edwinsoji).</description>
    <link>https://dev.to/edwinsoji</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4026348%2F7296192c-6ec7-48e1-a069-24e9f424ba72.jpg</url>
      <title>DEV Community: Edwin Sanjo Soji</title>
      <link>https://dev.to/edwinsoji</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/edwinsoji"/>
    <language>en</language>
    <item>
      <title>How to Set Up PostgreSQL with Docker on Ubuntu</title>
      <dc:creator>Edwin Sanjo Soji</dc:creator>
      <pubDate>Thu, 16 Jul 2026 03:33:18 +0000</pubDate>
      <link>https://dev.to/edwinsoji/how-to-set-up-postgresql-with-docker-on-ubuntu-1p90</link>
      <guid>https://dev.to/edwinsoji/how-to-set-up-postgresql-with-docker-on-ubuntu-1p90</guid>
      <description>&lt;blockquote&gt;
&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;This post walks through running PostgreSQL 18 on a fresh Ubuntu server using Docker&lt;br&gt;
Compose, with a named volume for data persistence, secrets in a &lt;code&gt;.env&lt;/code&gt; file, a&lt;br&gt;
healthcheck, and the port bound to localhost only (with an SSH tunnel for remote&lt;br&gt;
access). By the end you'll have a Postgres instance that survives container&lt;br&gt;
restarts, isn't exposed to the open internet, and is easy to back up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You need a Postgres instance on a server for an app, a side project, or a homelab&lt;br&gt;
setup. Docker Compose is the fastest way to get a reproducible setup — no manual&lt;br&gt;
&lt;code&gt;apt install postgresql&lt;/code&gt;, no fighting with system package versions, and the whole&lt;br&gt;
config lives in one file you can commit to git (minus secrets).&lt;/p&gt;
&lt;h2&gt;
  
  
  Environment
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Ubuntu 22.04 or 24.04 LTS
- Docker Engine (installed via Docker's official apt repo)
- Docker Compose v2 (the `docker compose` plugin, not the old standalone docker-compose)
- PostgreSQL 18 (official image from Docker Hub)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Approach / Key Decisions
&lt;/h2&gt;

&lt;p&gt;A few choices worth calling out before the config, since they're the parts people&lt;br&gt;
usually get wrong on the first try:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pin the image tag&lt;/strong&gt; (&lt;code&gt;postgres:18.4&lt;/code&gt;), not &lt;code&gt;latest&lt;/code&gt; — reproducibility matters
more than convenience once you have real data in the volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Named volume, not bind mount&lt;/strong&gt;, for the data directory — avoids permission
headaches between the container's internal user and the host filesystem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets in &lt;code&gt;.env&lt;/code&gt;&lt;/strong&gt;, never hardcoded in &lt;code&gt;docker-compose.yml&lt;/code&gt; — so the compose
file itself is safe to commit to git.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bind the port to &lt;code&gt;127.0.0.1&lt;/code&gt; only&lt;/strong&gt; — Postgres has no business listening on
&lt;code&gt;0.0.0.0&lt;/code&gt; on a public server. If you need remote access, use an SSH tunnel or a
VPN, not an open port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a healthcheck&lt;/strong&gt; — so anything depending on Postgres (an app container,
a CI job) can wait for "actually ready to accept queries," not just "container
started."&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Solution / Implementation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Install Docker Engine + Compose plugin
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# refresh package index and install packages needed to add a new apt repo over HTTPS&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ca-certificates curl gnupg

&lt;span class="c"&gt;# create the directory that will hold Docker's GPG key&lt;/span&gt;
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/apt/keyrings
&lt;span class="c"&gt;# download Docker's official GPG key and make it readable by apt&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/ubuntu/gpg &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.asc
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;a+r /etc/apt/keyrings/docker.asc

&lt;span class="c"&gt;# add Docker's apt repo, matching your CPU arch and Ubuntu codename automatically&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"deb [arch=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; stable"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/docker.list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null

&lt;span class="c"&gt;# pick up the new repo, then install Docker Engine + the Compose v2 plugin&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Let your user run docker without &lt;code&gt;sudo&lt;/code&gt; (log out/in, or &lt;code&gt;newgrp docker&lt;/code&gt;, after this):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# add your user to the docker group so you don't need sudo for every docker command&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="nv"&gt;$USER&lt;/span&gt;
&lt;span class="c"&gt;# refresh your shell's group membership without needing to fully log out&lt;/span&gt;
newgrp docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nt"&gt;--version&lt;/span&gt;         &lt;span class="c"&gt;# confirm Docker Engine installed correctly&lt;/span&gt;
docker compose version   &lt;span class="c"&gt;# confirm the Compose v2 plugin is available&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Set up the project directory
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# init-scripts/ will hold optional setup SQL/shell scripts (explained below)&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/postgres-docker/init-scripts
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/postgres-docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init-scripts/&lt;/code&gt; is optional — anything in there (&lt;code&gt;.sql&lt;/code&gt; or &lt;code&gt;.sh&lt;/code&gt; files) runs once,&lt;br&gt;
automatically, the first time the container initializes an empty data directory.&lt;br&gt;
Useful for creating extra databases, extensions, or seed data.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Create the &lt;code&gt;.env&lt;/code&gt; file
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# writes a .env file that docker-compose.yml will read via ${VAR} substitution.&lt;/span&gt;
&lt;span class="c"&gt;# NOTE: docker compose's .env parser doesn't strip inline "# comments" after a&lt;/span&gt;
&lt;span class="c"&gt;# value, so don't add trailing comments on these lines — they'd become part of&lt;/span&gt;
&lt;span class="c"&gt;# the value itself. Generate a real password with: openssl rand -base64 24&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
POSTGRES_USER=appuser
POSTGRES_PASSWORD=change_this_to_a_long_random_password
POSTGRES_DB=appdb
POSTGRES_PORT=5432
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# keep secrets out of git&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;".env"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  4. Create &lt;code&gt;docker-compose.yml&lt;/code&gt;
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:18.4&lt;/span&gt;          &lt;span class="c1"&gt;# pinned version, not `latest` — see "Key Decisions" above&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres_db&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;        &lt;span class="c1"&gt;# auto-restart on crash or host reboot (as long as Docker itself starts on boot)&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# values are pulled from .env in the same directory via ${VAR} substitution&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# bind to localhost only — NOT "5432:5432", which would expose it on all interfaces&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:${POSTGRES_PORT}:5432"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;              &lt;span class="c1"&gt;# named volume — persists data across container recreation&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./init-scripts:/docker-entrypoint-initdb.d&lt;/span&gt;   &lt;span class="c1"&gt;# optional: runs once against an empty data dir&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# checks Postgres can actually accept queries, not just that the process is running&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_DB}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;   &lt;span class="c1"&gt;# how often to run the check&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;     &lt;span class="c1"&gt;# how long to wait for a response before counting it as failed&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;      &lt;span class="c1"&gt;# consecutive failures before marking the container "unhealthy"&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="c1"&gt;# declares the named volume used above; Docker manages the storage location&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  5. Bring it up
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;        &lt;span class="c"&gt;# start the container in the background, reading .env + docker-compose.yml&lt;/span&gt;
docker compose ps           &lt;span class="c"&gt;# check it's running (and, once healthchecks pass, "healthy")&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; postgres   &lt;span class="c"&gt;# tail the logs; Ctrl+C to stop watching (container keeps running)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Wait for &lt;code&gt;database system is ready to accept connections&lt;/code&gt; in the logs, or check&lt;br&gt;
the healthcheck status in &lt;code&gt;docker compose ps&lt;/code&gt; (it should show &lt;code&gt;healthy&lt;/code&gt;).&lt;/p&gt;
&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;From inside the container:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# -it gives you an interactive terminal attached to the psql session&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; postgres_db psql &lt;span class="nt"&gt;-U&lt;/span&gt; appuser &lt;span class="nt"&gt;-d&lt;/span&gt; appdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once in &lt;code&gt;psql&lt;/code&gt;, sanity-check the connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;conninfo&lt;/span&gt;  &lt;span class="c1"&gt;-- shows which user/db/host/port you're connected as&lt;/span&gt;
&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;         &lt;span class="c1"&gt;-- lists all databases visible to this user&lt;/span&gt;
&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;         &lt;span class="c1"&gt;-- exit psql&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Healthcheck directly:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# same check docker-compose.yml's healthcheck runs internally — useful for manual debugging&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;postgres_db pg_isready &lt;span class="nt"&gt;-U&lt;/span&gt; appuser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;From the host&lt;/strong&gt; (if you have &lt;code&gt;psql&lt;/code&gt; installed locally — &lt;code&gt;sudo apt install postgresql-client&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# works because the port is bound to 127.0.0.1, i.e. reachable from the host itself&lt;/span&gt;
psql &lt;span class="nt"&gt;-h&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;-p&lt;/span&gt; 5432 &lt;span class="nt"&gt;-U&lt;/span&gt; appuser &lt;span class="nt"&gt;-d&lt;/span&gt; appdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;From a remote machine&lt;/strong&gt;, since the port is bound to localhost only, tunnel in first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# forwards local port 5432 through SSH to the server's 127.0.0.1:5432&lt;/span&gt;
ssh &lt;span class="nt"&gt;-L&lt;/span&gt; 5432:127.0.0.1:5432 youruser@your-server-ip

&lt;span class="c"&gt;# then, in a separate terminal on your local machine, connect as if Postgres were local:&lt;/span&gt;
psql &lt;span class="nt"&gt;-h&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;-p&lt;/span&gt; 5432 &lt;span class="nt"&gt;-U&lt;/span&gt; appuser &lt;span class="nt"&gt;-d&lt;/span&gt; appdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lessons Learned / Gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;docker compose down&lt;/code&gt; vs &lt;code&gt;docker compose down -v&lt;/code&gt;&lt;/strong&gt; — the first keeps your
named volume (data survives); the second deletes it too. Easy to fumble this
when you're tearing down containers to "start fresh."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changing &lt;code&gt;POSTGRES_USER&lt;/code&gt;/&lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;/&lt;code&gt;POSTGRES_DB&lt;/code&gt; after the volume
already has data does nothing&lt;/strong&gt; — those env vars only take effect on first
init of an empty data directory. To change them later, update the values with
SQL directly, or wipe the volume and start over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never expose 5432 to &lt;code&gt;0.0.0.0&lt;/code&gt; on a public server.&lt;/strong&gt; Postgres auth is not
designed to be your only line of defense against internet-wide scanning and
brute-force attempts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backups are still your job.&lt;/strong&gt; A container restart is not a backup strategy.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;# dumps the database to a plain SQL file on the host, named with today's date&lt;/span&gt;
  docker &lt;span class="nb"&gt;exec &lt;/span&gt;postgres_db pg_dump &lt;span class="nt"&gt;-U&lt;/span&gt; appuser appdb &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backup_&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;.sql

  &lt;span class="c"&gt;# restore into a fresh instance (-i keeps stdin open so the file content reaches psql)&lt;/span&gt;
  &lt;span class="nb"&gt;cat &lt;/span&gt;backup_2026-07-16.sql | docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; postgres_db psql &lt;span class="nt"&gt;-U&lt;/span&gt; appuser &lt;span class="nt"&gt;-d&lt;/span&gt; appdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;restart: unless-stopped&lt;/code&gt;&lt;/strong&gt; means Postgres comes back up after a server
reboot — but only if the Docker daemon itself is set to start on boot
(&lt;code&gt;sudo systemctl enable docker&lt;/code&gt;, usually on by default after the apt install).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hub.docker.com/_/postgres" rel="noopener noreferrer"&gt;PostgreSQL Docker Hub image docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/reference/compose-file/" rel="noopener noreferrer"&gt;Docker Compose file reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/engine/install/ubuntu/" rel="noopener noreferrer"&gt;Docker Engine install docs for Ubuntu&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>linux</category>
      <category>postgres</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
