this post was submitted on 04 Feb 2025
3 points (100.0% liked)

Docker

1182 readers
4 users here now

founded 2 years ago
MODERATORS
 

I recently asked the best way to run my Lemmy bot on my Synology NAS and most people suggested Docker.

I'm currently trying to get it running on my machine in Docker before transferring it over there, but am running into trouble.

Currently, to run locally, I navigate to the folder and type npm start. That executes tsx src/main.ts.

The first thing main.ts does is access argv to detect if a third argument was given, dev, and if it was, it loads in .env.development, otherwise it loads .env, containing environment variables. It puts those variables into a local variable that I then pass around in the bot. I am definitely not tied to this approach if there is a better practice way of doing it.

opening lines of main.ts

import { config } from 'dotenv';

let path: string;

const env = process.argv[2];
if (env && env === 'dev') {
    path = '.env.development';
} else {
    path = '.env';
}

config({
    override: true,
    path
});

const {
    ENVIROMENT_VARIABLE_1
} = process.env as Record<string, string>;

Ideally, I would like a way that I can create a Docker image and then run it with either the .env.development variables or the .env ones...maybe even a completely separate one I decide to create after-the-fact.

Right now, I can't even run it. When I type docker-compose up I get npm start: not found.

My Dockerfile

FROM node:22
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
USER node
COPY . .
CMD "npm start"

My compose.yaml

services:
  node:
    build: .
    image: an-image-name:latest
    environment:
      - ENVIROMENT_VARIABLE_1 = ${ENVIROMENT_VARIABLE_1}

I assume the current problem is something to do with where stuff is being copied to and what the workdir is, but don't know precisely how to address it.

And once that's resolved, I have even less idea how to go about passing through the environment variables.

Any help would be much appreciated.

top 5 comments
sorted by: hot top controversial new old
[–] [email protected] 3 points 2 weeks ago
[–] onlinepersona 2 points 2 weeks ago (1 children)

Concerning environment variables, you can use volumes in the compose.yaml to bind mount files or directories in your container e.g

services:
  node:
    volumes:
      - ./prod.env:/usr/src/app/.env
      - ./dev.env:/usr/src/app/.env.development

@[email protected] is probably right about the CMD. Read the documentation and learn about the two modes CMD has. Try to figure it out yourself and if you can't, reveal the spoiler below

What I think should workCMD ["npm", "start"]

runs npm with the start argument. What you passed was like running "npm start" in your shell. It looks for a command that's literally called "npm" "space" "start", which of course doesn't exist.

Anti Commercial-AI license

[–] [email protected] 1 points 2 weeks ago (1 children)

Yeah I actually originally had it in the ["one", "two"] format, but it was weird because before the arguments I actually wanted the source I got it from had put something along the lines of "sh", "-p" (it was definitely launching into a shell of some sort, with some kind of flag, but I forget exactly what). I removed those, and at the same time removed the square brackets, but messed up by keeping the quote marks.

Re using volumes, am I understanding this correctly:

That would mean that when standing up the container from the built Docker image, it contains the two env files, prod and development? Is there, alternatively, an easy way to only have one in the container, and choose which in the docker-compose up command?

[–] onlinepersona 1 points 2 weeks ago (1 children)

It's probably "sh" "-c" that was being used. That's the ENTRYPOINT. CMD is passed to the ENTRYPOINT. The docs explain it well, I think.

As for volumes, TL;DR these are mounted into the container at runtime and are not in the image. You may put the dev and production config in the image, but I'd advise against it, especially if the production config contains secrets. Then they'll be baked into the docker image.

long explanationA docker image is basically an archive of archives. Each line you put in a Dockerfile is executed and the result thereof is put into an archive with a name (hash of the contents plus maybe other stuff) and that's called a layer. To keep the layers small, they contain only the updates to the previous layer. This is done with Copy On Write (CoW).

FROM alpine
RUN touch /something # new archive with one single file
RUN apk add curl # new archive with paths to curl WITHOUT /something

To run a docker image aka docker run $options $image. The archive and its layers are extracted, then a new runtime environment is created using linux kernel namespaces (process namespace, network namespace, mount / storage namespace, ...), and the sum of the layers is mounted in there as a "volume". That means the processes within the container are segregated from the host's processes, the container's network is also segregated, the storage / files and folders too, and so on.

So there's a, for lack of a better term, root/runtime volume (/) from the sum of layers. So the container only sees the the layers (plus some other fundamental stuff to run like /dev, /proc, etc.). You can mount a file into the container, a folder, a device, or even a network drive if you have the right driver installed. Mounting a file or folder is called a "bind mount", everything else (if I'm not mistaken) is just a mount. Volumes in docker are built on top of that. The doc explains in detail what volumes are and their naming.

In conclusion, docker image = archive, docker container = extracted archive running in linux namespaces, volume = storage that be mounted into container.

Finally, parameterising compose files: use environment variables. You can put then into a .env beside your compose.yaml or set them before calling e.g ENVIRONMENT_VARIABLE=someValue docker compose up.

They are "interpolated" aka used in the compose file like so

services:
  web:
    image: "webapp:${TAG}"

I recommend you read the docs on the syntax, but for your purpose I'd use something like

volumes:
  - ${ENV_FILE:?Env file not provided}:/usr/src/app/.env

Then you can ENV_FILE=prod.env docker compose up or put the env var in .env and docker compose will pick it up.

Anti Commercial-AI license

[–] [email protected] 1 points 2 weeks ago* (last edited 2 weeks ago)

these are mounted into the container at runtime and are not in the image

Yeah thanks, that's what I thought was the case.

Personally I think I'd rather not do what you had in the earlier comment, because it feels wrong for the container to know that it's running in dev or prod mode. I like the suggestion of passing in the ENV_FILE that you gave in this comment. But I am struggling a little, because at the moment it seems I need to both put the env filename in the .env itself (which is just silly in its own right) and pass it through with --env-file (or a local environment variable). ~~I can't quite work out why, and the documentation is absolutely no help. I saw some one thread where it was said that with docker-compose, --env-file should set the variable in the compose file, but when I do~~

docker-compose --env-file .env up

~~I get required variable ENV_FILE is missing a value: Env file not provided which goes away if I put the seemingly-redundant ENV_FILE variable in the .env file.~~

edit: Worked it out. It seems the issue was my compose.yaml environment variables were in the format - COMMUNITY_NAME = ${COMMUNITY_NAME}, but it should have been - COMMUNITY_NAME=${COMMUNITY_NAME}, without the spaces.

Now if I do docker-compose up -d it takes the .env variables, but I can also do docker-compose --env-file .env.development up -d to specify an alternative env file. Which actually isn't what I expected (I expected --env-file to be compulsory), but honestly as far as I'm concerned it's even better this way.