1---
2title: "Local services: Postgres with containers, networks, and volumes"
3description: Run a local Postgres for development as alchemy resources — a pinned image, a named network and volume, a Redacted password, and a healthchecked container that converges on every deploy.
4---
5
6A Docker `Container` runs an image; a `Network` gives containers a
7stable DNS name to reach each other; a `Volume` keeps data alive
8across container replacements; a `RemoteImage` pins exactly which
9image you run. In alchemy all four are resources in your Stack, so
10your local dev dependencies get the same treatment as your cloud
11infrastructure: a reviewable diff on every change, one deploy that
12converges Docker to the declared state, and one destroy that tears
13it all down.
14
15You'll need a Docker daemon reachable from your CLI and the provider
16registered — see [Setup](/docker/setup) and the
17[Docker overview](/docker). The full program below is the checked-in
18[`examples/docker-postgres`](https://github.com/alchemy-run/alchemy/tree/main/examples/docker-postgres).
19
20## Pin the image
21
22```typescript
23import * as Alchemy from "alchemy";
24import * as Docker from "alchemy/Docker";
25import * as Effect from "effect/Effect";
26import * as Layer from "effect/Layer";
27
28export default Alchemy.Stack(
29 "DockerPostgresExample",
30 {
31 providers: Layer.merge(Docker.providers(), Alchemy.RandomProvider()),
32 state: Alchemy.localState(),
33 },
34 Effect.gen(function* () {
35 const image = yield* Docker.RemoteImage("postgres-image", {
36 name: "postgres",
37 tag: "18-alpine",
38 alwaysPull: false,
39 });
40 // ...
41 }),
42);
43```
44
45`RemoteImage` pulls `postgres:18-alpine` through your Docker CLI,
46and `alwaysPull: false` pins it — once the image is present locally,
47subsequent deploys diff to a no-op instead of re-pulling on every
48run. The Stack uses `Alchemy.localState()` because there's no cloud
49account behind these resources, and merges in
50`Alchemy.RandomProvider()` for the generated password below.
51
52## A network and a volume
53
54```typescript
55const network = yield* Docker.Network("app-network");
56const data = yield* Docker.Volume("postgres-data");
57```
58
59Neither needs any props — the engine generates unique physical names
60from the app, stage, and logical ID. Note that changing a `Network`
61or `Volume` prop (driver, labels, IPv6) is a replacement: the old
62one is deleted and a new one created, so treat a Volume's props as
63fixed once it holds data you care about.
64
65## A Redacted password
66
67```typescript
68import * as Config from "effect/Config";
69import * as Option from "effect/Option";
70
71const configuredPassword = yield* Config.Redacted("POSTGRES_PASSWORD").pipe(
72 Config.option,
73);
74const password = yield* Option.match(configuredPassword, {
75 onSome: Effect.succeed,
76 onNone: () => Alchemy.makeRandom("PostgresPassword", { bytes: 16 }),
77});
78```
79
80If `POSTGRES_PASSWORD` is set in the environment it's used as-is;
81otherwise `Alchemy.makeRandom` generates a stable random secret
82(this is what `RandomProvider` in the Stack config is for). Either
83way the value is `Redacted`, and `Container` passes environment
84values to the daemon via the process environment — only the keys
85appear on the `docker` argv, so the secret never lands in shell
86history or a process listing.
87
88## The container
89
90```typescript
91const postgres = yield* Docker.Container("postgres", {
92 name: "alchemy-example-postgres",
93 image,
94 environment: {
95 POSTGRES_DB: "app",
96 POSTGRES_USER: "alchemy",
97 POSTGRES_PASSWORD: password,
98 },
99 ports: [{ external: 15432, internal: 5432 }],
100 volumes: [
101 { hostPath: data.name, containerPath: "/var/lib/postgresql/data" },
102 ],
103 networks: [{ name: network.name, aliases: ["postgres"] }],
104 healthcheck: {
105 cmd: ["CMD-SHELL", "pg_isready -U alchemy -d app"],
106 interval: "5 seconds",
107 timeout: "5 seconds",
108 retries: 10,
109 },
110 start: true,
111});
112```
113
114`image` accepts the `RemoteImage` resource directly — no manual ref
115plumbing. A `hostPath` set to a volume's name mounts the named
116volume rather than a host directory, the `postgres` network alias
117gives other containers on `app-network` a stable DNS name, the
118healthcheck intervals take Effect `Duration` strings like
119`"5 seconds"`, and `start: true` actually runs the container
120(the default creates it stopped).
121
122## Read the bound ports
123
124```typescript
125// as Stack outputs:
126return { container: postgres.name, hostPort: postgres.ports };
127
128// or live, from any Effect with the Docker provider:
129const runtime = yield* Docker.inspectContainer("alchemy-example-postgres");
130```
131
132The container's `ports` attribute is a record keyed by container
133port and protocol — here `"5432/tcp"` maps to `15432` — which is
134what your app's connection string needs; `Docker.inspectContainer`
135reads the same normalized attributes fresh from the daemon for any
136container by name, bound host ports included.
137
138## Where next
139
140- [Build & push an image](/docker/build-and-push) — build your own
141 image from a Dockerfile and push it to a registry.
142- [Docker overview](/docker) — all Docker resources at a glance,
143 including how adoption of pre-existing containers, networks, and
144 volumes is gated behind `adopt`.
145
146Reference:
147
148- [Container API reference](/providers/docker/container)
149- [RemoteImage API reference](/providers/docker/remoteimage)
150- [Image API reference](/providers/docker/image)
151- [Network API reference](/providers/docker/network)
152- [Volume API reference](/providers/docker/volume)