#how do i deploy django on docker and connect it to domain locally?
1 messages ยท Page 1 of 1 (latest)
You could edit the dns registry to point to your dynamic ip, but there might be security concerns. Not too sure.
It should be not that different from running it in the cloud...
You would need to configure your router to forward the appropriate ports to your containers.
If its only hosted locally and not exposed to the big bad internet, then you don't have to worry about 99% of security concerns.
If someone is in your network without you knowing, some rando web server is the least of your security problems...
If you want to deploy it locally you'll need a couple of things which most cloud infrastructures abstract away for (or against :P) you.
A host for running all of this (if you want to keep it simple, you can just run everything on one host)
You have two routes for figuring out DNS:
- Tell your router (which acts as your primary dns in most cases) to point your domain to the internal ip of your host
- OR: Change up your hosts file, but if you want to use the dns name on multiple devices in your network, then you'll have to do this on every host
Docker is not really different in the cloud than running locally (the beauty of a containerized approach)
Install docker on the host and you're pretty much set
CDN's are overkill for a little app only you will realistically use. Even a few dozen or maybe hundreds of users can be totally fine without using a CDN
Just use nginx for serving static files and set the cache expiration to some days and you'll have yourself a makeshift CDN
My regular stack goes like this (for simplicity and maintainability)
Postgres (db) -> Django -> Gunicorn -> Nginx (For handling static and media file serving)
All of that is running inside docker, of course.
If you need help with your Dockerfile, just drop it in here and someone (maybe me) will take a look. Most of the time its relatively easy to get things going but you may have some small hick-ups which can drag on for some time
In any case. You don't particularily have to worry about security concerns when not exposing your app to the internet
If you plan on doing that, then you'll have to look into DynDNS setups (as your public IPv4 will probably change over time) and some more security precautions like rate limiting and not exposing django-admin to the world
Other than that you're fine ๐
Hundreds of thousands of monthly hits is also often fine without a CDN, if your customers are close enough ๐
An e-commerce site I worked on more often then not went without it.
It all depends on what kind of data we're talking about of course, if its a heavy multi mb css file for every view, then a cdn is probably a good idea
But you can achieve amazing things by simply adjusting caching settings on your staticfiles
A CDN isn't doing much more, they're not magic. Its just that they can handle a lot more traffic most of the time
Thats a lot of thumbs up xD
Don't fear docker
Start building, you can hardly break anything ๐
I generally use separate stages for dev and production anyway.
Just get going
The facebook ethos of "move fast and break things" applies to some extend
I've been doing that for years, and yet i have yet to actually find a convenient way to cache the intermediary stages though
It can be a worthwhile endeavor to learn, but no one builds their own images from scratch for real code ๐
Unless you build something in like go of course.
The beauty of docker images is that you can build ontop of a well defined base image
For a django project you'll likely use the python image as a base
For the stack i've described above it normally uses the following images:
python
nginx
postgres
Thats it. Not much more to it
I mean, i consider myself a go enthusiast (if something like that even exists xD)
We don't really do that
We just take the alpine image and put the compiled binary in there and off you go
Yeah. The point is more that it's feasible. Not that you should :p
Single binary is ok, but saving 10 MB of Alpine isn't a massive boost.
Yea, damn, now i want to create images which have not much more than the size of the binary xD
PLENTY xD
My smallest test-vm is running on 512mb ram running debian, and a fullstack app (built with django)
Swapping is disabled, but it gets to sweat when i recreate the containers
Traefik, 2 django containers, 2 nginx, 1 postgres right now, IIRC
2Gb is plenty for something like debian and a small docker-deployed django app
- When you manage to exceed that limit by yourself, consider you may have to optimize ๐
an explicitly empty image, especially for building images "FROM scratch"
RAM usage from one of my server nodes:
48.81% (61.36 GiB of 125.70 GiB)
I can maybe give that vm a bit more ram tbh xD Its suffering for no reason xD
Thats just one of my compute nodes, not just one vm xD
There are a bunch of other things running, not just web apps
My smallest test vm is just 512mb
There is a thousand things which should be taught at universities and schools in general, yet here we are.
But thats a discussion for another time (namely another 3-5am rambling session in #offtopic-chat xD)
- https://gitlab.com/theepic-dev/mapper/-/tree/main/django?ref_type=heads
- https://gitlab.com/theepic-dev/mapper/-/blob/main/django/Dockerfile?ref_type=heads
- https://gitlab.com/theepic-dev/mapper/-/blob/main/django/requirements.txt?ref_type=heads
- https://gitlab.com/theepic-dev/mapper/-/blob/main/docker-compose.yml?ref_type=heads
- https://gitlab.com/theepic-dev/mapper/-/blob/main/dev-templates/override.yml?ref_type=heads
Have a look at these files... I copy the override.yml to docker-compose.override.yml in the project's root dir, and use that for dev.
Package installation occurs in the build stage, hence defined in the Dockerfile.
To create a prroject, I generally set the ENTRYPOINT to sleep 10000000000 and then just run docker exec -it django_container_name bash and run django-admin startproject some_name . ...
docker exec is a great way to run all the manage.py commands.
Postgres the DB? I run it in its own container.
Psycopg for the python adatper is listed in requirements.txt... Though if you use Django 4.2, you should use psycopg 3, not 2.
ARG PYTHON_IMAGE=python:3.11-slim-bullseye
FROM ${PYTHON_IMAGE} as django-base
Should probably use bookworm too. I always forget the fucking Debian release names ๐
But just use one of the official python images from Docker Hub as a base. You can even choose the specific python version to use if you need an older one...
I usually just use a postgres image... no need for a Dockerfile for those.
docker-compose*.yml allows you to orchestrate multiple containers.
In it I tell docker to build the Django container, and pull the stock postgres image.
Yeah. I also have a node.js one in dev and build stages.
I use postgis because I map some data... you can use postgres if not using GeoDjango
You do need to install docker on the host os so it can run containers.
You can install it in different ways, e.g. a virtual machine, but if you use Linux, it's often good to just install it straight on the host, yes. At least I always do.
You probably want python installed on the host so you can run other python scripts, the shell, etc. Not everything needs to be dockerized.
But your Django projects should run in docker.
(Typically)
Can be either. Docker or virtual env.
At least on EC2.
There are services meant specifically for docker or kubernetes.
Yeah, I develop straight in docker from the get go.
The official python container images actually include a lot more than just the interpreter.
They include a lightweight Linux distro, including the system libraries.
Either based on Debian or Alpine.
Bookworm is Debian.
I've never used the windows images
Bullseye I think was the last version of Debian before bookworm.
Mostly it's convenient. You can do it all yourself... but why? ๐
Yup. Well, Django lives in a python container.
I wouldn't say another OS...
Since the containers don't actually have a kernel.
They share the kernel with the host.
The container has its own system libraries and package manager, but I think it doesn't count as an OS without a kernel ๐
But I may just be being pedantic.
Well, pulling the image just makes it available locally. I practically never run pull commands ๐
I typically start with a docker compose and a dockerfile
And run docker compose up --build which pulls it automatically.
On a remote server?
Sure it is
You might want to use tmux to ensure your sessions don't drop mid install...
And enable the keepalive to not get random disconnections.
Depends...
Git is tracking source code
Mostly. You can use it for documents.
If you use configuration management tools like Ansible, you should keep their config in git.
You can use git to deploy your code to production
It makes deployment a lot easier.
Yeah, though that's normal. Even if you specify the tag it's usually 300+MB
If you run Docker pull, you just pulled an image... so it's not running.
Just copied the data from docker hub to your server
Yeah, that's not surprising
Doesn't matter either.
You still want it dockerized for the Django projects...
Isolate dependencies
Use reproducible builds
Do you have a Django project?
But you can't run Docker locally yet?
Well, you can always dockerize an existing project.
Not a bad place to start. Might be a good idea to check the code structure first.
You don't use git at all now?
That doesn't have to be fancy ๐
You can git clone and git pull to deploy. You don't need to use CI/CD tools.
Yeah, I usually start with docker compose
Then add a dockerfile
Though I'm in the process of templating it
Slowly because I don't like cookiecutter.
Minimalistic dockerfile for django?
FROM python:3.10.6-alpine
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . /app
RUN python3 /app/manage.py collectstatic --noinput
CMD ["gunicorn", "YourApp.wsgi", "--bind", "0.0.0.0:8000"]
Off you go
Although that lacks a proper entrypoint which handles migrations and whatnot
The only other thing thats really missing apart from a real entrypoint which does some nice housekeeping things, is the compilemessages step ๐
YourApp is the name of the project's folder (the folder in which the settings.py lives)
and the wsgi references the wsgi file in that same folder
its telling gunicorn, that it should run that file to get going, and from there on out its pretty much off to go
The workdir has nothing todo with that.
Its just that i put the app's code into the /app folder in the container
and set that as the workdir, so i don't have to always prefix all my paths with the /app
You can call it whatever you want, just keep it consistent when you use absolute paths (which you'll probably rarely do anyways)
Don't run collectstatic in the docker file
You can theoretically just do COPY . . instead of COPY . /dockerApp (don't ask me why i did that there)
I've always hated that stance, it just doesn't make sense
And yes, i know that its hard(er) to get the container folder to actually supersede the mounted folder
Mount a shared volume between python and nginx and collect on startup
You can define a volume in your dockerfile which will always win container side, over any mounted points
How? I've never done that ๐
https://docs.docker.com/engine/reference/builder/#volume
Does the trick
I know, it sounds like a hack, because it is.
But its simply amazing xD
Dockerfile reference link Docker can build images automatically by reading the instructions from a Dockerfile. A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image. This page describes the commands you can use in a Dockerfile.
Format link Here is the format of the Dockerfile:
c...
You can do a VOLUME ["/static"] from the dockerfile of your app, and then tell the nginx container (or any other container which wants to mount that dir), to mount it using the docker-compose config for volumes_from
volumes_from:
- app:ro
That way the app's content will always have precedent, you won't waste extra space for essentially storing the image's folder AND your copy on the host (which you have to rebuild on every startup otherwise)
It makes startup faster and the image is always ready to go, no matter if someone runs the entrypoint or not
Cheers, I'll have to try that ๐
Normally, precedence is pretty easy in containers:
If you mount a folder of some container on your host, the The host's version will always win.
Except if the host is empty and the container isn't, in which case docker will just copy everything to the host and we're back where we started
the issue is, if you do a collectstatic, and you release another version with a different resulting collectstatic, that the folder is already there on the deployed servers, and as such, the old version will be kept instead of the new one
Thats why most people tell you to just run collectstatic as part of your startup routine
However, due to a little unknown implementation detail of defining a volume in the dockerfile and directly using that volume from another container, you can basically tell docker, that if container x (nginx for example) asks for a file in that dir, it should get the files mounted in container y (your app container)
No copying done, no weird fuzz, just easy as pie
If you want to keep it simple
You can of course go for the collectstatic on startup, it won't make your app's startup unbearabily slow (thats why most people just accept it)
I just despise the fact that my startup is doing something which should've been done whilst building the image
And just a fyi:
Podman can do this natively without some weird hack, just something to think about
Podman ftw xD
You can choose between doing something whist building the image (far before anyone will run it)
And whist running the container (like an entrypoint)
There isn't really a dockerize, there is containerization though
It means the general principal of putting something in the form of a container
Docker is merely one of many container platforms (although it is the biggest one probably)
In my opinion, yes
But again, if you find that it introduces problems down the line, because maybe docker doesn't like that feature at all, and they remove it, then don't do it.
Docker volumes as they normally work, mounting the hosts folder with precedence will pretty much always be a thing
I've done it for a few customer projects and its been running for a few years without issues (Although their IT staff is probably too lazy to actually update their docker clusters, so take it with a grain of salt xD)
Django -> Gunicorn (Running with the wsgi file) -> Nginx
Works fine just fine
Thats probably the most common setup
Do you mean your requirements.txt?
Nginx and postgres are not python packages, and as such cannot be installed via pip (through your requirements.txt)
In fact, postgres and nginx are going to be separate containers
The requirements.txt is only for pip
Its only convention to call it that, you could call it whatever you want
Yes
Its just running pip install and its taking the package names from the file
You only need a dockerfile if you want to build or change a container image.
The default postgres image should suffice
You'll have to do another dockerfile for the nginx one as you have to replace the default nginx.conf with your own
Every separate "thing" has its own container
The dockerfile isn't used to run anything besides giving the instructions on how to create the image.
Just a bit about the terminology:
Image -> The stencil, or blueprint for containers, it contains all the necessary binaries
Container -> One instance of an image. It runs stuff
Containers are ephemeral, if you delete a container (not stop it, actually delete it), its data is gone as well - kinda makes sense
To preserve data, we can mount folders from the host and write into those folders
Those will be persisted, as long as you don't manually delete them, of course. Containers are supposed to not store any actual data outside of volumes/mounts, recreating a container (and does deleting it) should not result in any data loss - apart from stuff which was in transit if you killed the container
The drives for a VM don't necessarily have a single file... and they are entirely different concepts.
A VM has its own kernel, and doesn't even need to be the same OS.
Containers use Linux kernel features like cgroups, overlay FS, etc. to isolate execution environments but still share the same kernel with the container.
I'll refer back to my last statement:
Containers are ephemeral, if you delete a container (not stop it, actually delete it), its data is gone as well - kinda makes sense
To preserve data, we can mount folders from the host and write into those foldersThose will be persisted, as long as you don't manually delete them, of course. Containers are supposed to not store any actual data outside of volumes/mounts, recreating a container (and does deleting it) should not result in any data loss - apart from stuff which was in transit if you killed the container
You can actually tell docker to basically make a snapshot of a container and save it as a file, and you can tell docker to import that container.
You should only use that feature for debugging.
Yes, apart from the stuff which is mounted
If you mount nothing, and recreate the container, its a completely fresh start, like the previous run didn't happen
(If its a closed system in the sense that it doesn't rely on outside data sources which may have changed, of course)
The starting image is normally pretty easy to pick, its what you do with your image thats important
If its a container which is meant to run gunicorn, then the python image is your go-to
Database? Postgres for example has one ready to go
You need nginx? Take the nginx image.
Convenience wrapper. You can skip docker-compose and pass a million arguments to docker directly.
It sucks tho
Imo migrate should ALWAYS be called on startup.
If its not, then your database will be in a state not consistent with the applocation and you will have problems
There is no single scenario in which that makes sense
I usually RUN mkdir or RUN useradd to create a directory in the container, set the WORKDIR and either COPY the files over, or use a bind mount (for dev).
Some people like using /app/... I usually use /django/ or /wagtail/ as the workdir so I always know what I am looking at ๐
No docker-compose file?
There are many ways ๐
Including no files...
I always use docker-compose... it makes everything easier.
Not sure who says it's not good for production.
I've deployed a bunch of apps with it.
The GUI is a useless piece of trash
- Its license prevents you from using it in a professional setting without paying copious amounts of money
The CLI is all you need and its got your back ๐
You have to build an image, and then you can run a container using that image.
When in development, you can also mount your project folder in the container, that way you don't have to rebuild the image every time you change one line
You then only have to rebuild it when you need to make changes to the run environment like installing dependencies
That stuff a few days ago was about not having to run collectstatic in your startup, but rather when building the image
Well, using the "slim" python image seems unnecessary, its not recommended for setups where space is plentiful (as noted in the README of the image)
Upgrading pip in the container is imo a bit of a terrible idea, but i can live with it
giving the command in the docker-compose instead of providing one as a default in the dockerfile seems unhelpful
using the os module for getting environment variables is surely fine, but if you're interested, have a look into django-environ, it has some nice things specifically for django
Hes using an .env file, but the postgres credentials are in the docker-compose for some reason
Using a named volume (in the db service) and using just a host mount looks weird (i'm nitpicking here)
You could already use psycopg3 instead of the v2, which also seems to finally properly support the "binary" variant
For some bizarre reason hes running the database migration manually
The nginx image setup looks fine, just make sure before you push it to also include a server_tokens off in your config, for not telling the world your nginx version
Maybe i'm a bit too harsh because i despise test driven development and their entire blog is literally called testdriven, but should be fine for learning the basics of containerizing a django app
Thats where most people put it, yea
I don't ๐
Me neither, although i do symlink to there sometimes
I have a project root containing docker-compose*.yml
Then nested dirs (assuming monorepos, which I mostly work with)
django/, node/, nginx/
Or similar
django/requirements.txt is where the reqs file lives.
docker directory with sub-dirs for all dockerfiles and other stuff like a nginx.conf for the nginx dir
PyCharm knows which docker-compose to pick with the git label
folder
||Friendship with JaK ended|| ๐
Also quite nice for anyone not using PyCharm cause they can just symlink to the root and not have to juggle with the default
dunno what you mean
Lemme burst the language bubble, for a german both are the exact same xD
We don't have a word for a directory, but we do have one for folder
Although i could explain the meaning of a directory with just one word which is probably one page long
Yeah, as a polyglot, I get the whole not-english thing ๐
I often type folder too
(most of the time I correct myself before hitting enter ๐)
Dateiorganisationsabheftungsvorrichtung is the best i could do for a german word
I've been to a Berufsweltmeisterschaft (IIRC) as a teenager and have loved the German language ever since.
Wo ist mein Bier?
One is right to my side
Dunno where you keep your beer (other than a fridge)
Depends on the beer ๐
Yea, that way even non german speakers can understand the important bits of a conversation
Don't do the Schwarzenegger
I usually don't keep IPAs in the fridge. (Can I highjack this thread for beer talk now? ๐)
Lets just move to #offtopic-chat, this thread is enormous as is already xD
Praying ain't gonna help
Did it print an error?
thats good
(Not to state the obvious)
But that is good.
You just built the image, you haven't started a container yet i'm guessing
The container will only run as long as its running command doesn't exit (The one you set in the dockerfile, or overwrote in the docker-compose)
Yep xD
I didn't even scroll up that high xD not on a pc right now
/usr/src/app
Thats what you set in your dockerfile
Run it without -d
If the container isnt running then you cant exec into it, but you can run it in the foreground by omitting the -d flag
Daemonizing doesn't help debug errors ๐
-d means daemonizing
Run in detached mode
"Running in the background" in easier terms
Sure you would
Docker compose logs
All stdout and stderr is captured and saved as the container's logs, you can view them using the docker compose logs command
Run it with docker compose
Docker compose runs everything with the context of your docker-compose file
It runs the appropriate docker commands in the background
Docker run can be used when you know the container name
Docker compose is doing exactly nothing more than what docker exposes, its just a nice wrapper which allows you to put all your configuration into one readable, clearly defined file
Otherwise you would have to run a dozen or more docker commands to get where you need to go
The blog post you sent had a netcat call in the entrypoint which would delay the app start until the database port was open
Your app just tried to connect to the database before it was ready
When the db doesnt have to initialize its startup is obviously a lot faster, thats why the second run worked
Basically a race condition
There are other ways of waiting for the db, but that one should be fine
I'm going to wager a guess
Debug is not off.
Another guess: Thats just nginx not finding the staticfiles
Yup. Check the nginx container logs.
You can also run a shell inside the containers to investigate what they see exactly...
docker exec -it app-nginx-1 sh...
Sometimes you can run bash, sometimes only sh is available.
Then you can ls /mnt/static/ (replace the path with your static files volume) to see if the files are indeed there.
There ain't no such thing as a docker docker container
I mean there is, its just that you probably shouldn't be using it xD (https://hub.docker.com/_/docker)
Docker in Docker!
You don't necessarily have to have two entrypoints btw, it may make more sense to share a single more configurable one
Just throwing that one out there
The separate yamls are fine
The entrypoints are basically what docker starts up when you start the container
Thats why i don't usually recommend people to random blogs or articles because they may have produced something which works, but why or how it works is completely outside of their grasp
I'd recommend reading a bit into the docker docs, they go through a lot of stuff and don't show something "just for django"
They show you the general principal and have you figure it out along the way
https://docs.docker.com/get-started/
That thing waits periodically until the database port is not blocked
and then it runs the command docker passed to it $@ refers to exactly that command
Yeah, people sometimes ask me for blog recommendations and I'm like... "I've mostly hacked all my (Docker) knowledge together using the docs over the last 5 years" ... Don't ask me what blogs are actually not shite ๐
- Learning using blogs or articles revolves around the idea of someone else having done what you are attempting to do
That may seem feasible, we're not all building new mars rovers overall, but it doesn't help you understand the pieces you have to put together and why they fit into each other
"Just that That thing works, and that other thing doesn't"
I could go on an entire philosophical tangent which just hates on blogs for tech stuff, but i'll just leave it at:
Understanding isn't watching someone else doing it. Getting better at something is 100% simply doing, or at least trying, to do that thing.
If someone asks me how do i get better doing thing X, i will and have always told them that they should be doing X, not search for "the best way of learning it", as they will inevitably spend more time searching for a path instead of walking it
A blog can't foresee every single deviation ๐
I sometimes do watch tutorials on YT and skim through blogs...
I don't hate them, but you definitely need to actually implement the knowledge and shouldn't expect that it's complete.
But not to copy an approach, or copy the goal, rather to understand the idea behind it i reckon
Yeah... you get the basics, then you can expand and adapt it to your needs.
E.g. I love the Getting Started with Ansible series by Learn Linux TV on youtube.
It taught me the basics, playbooks, templates, roles, etc. Then I adapted it to my processes for setting up a VPS or my own machines.
Like i've said, i'm not going on a philosophical tangent, i gotta stop with those at some point xD
We're still very much willing to help, but spoon-feeding isn't going to get you to where you want to go
@wintry fox you are now faced with the task of figuring out why your staticfiles don't find their way to the client's browser, think about what parts are in the chain for this to work and check them one by one
Now I barely ever run sudo pacman -S... or edit my neovim config. When I edit the config, it's in my setup repo and I use ansible to deploy it.
Good way to sync my dev env on my laptop and desktop.
They didn't progress through philosophical tangents
You had the order reversed
The philosophical tangents were a result of them having a stable system which allowed for such adventures
Its on YouTube, yes
But its about ansible, not docker or anything btw
I do use Ansible with docker, of course ๐
It installs my gitlab-ci runners, the docker engine, etc.
Yeah, I compare it ๐
Its not calling it, its reading out the env variables
Yep
Maybe have a look into django-environ, it has some nice things for reading in database configuration and django specific stuff
DEBUG = (os.environ.get("DJANGO_DEBUG") == "True")
I think i even recommended django-environ somewhere in this thread before xD
I've had a fairly busy weekend, so I didn't see everything that was discussed or know your code ๐
So no, I didn't pretend to not see an error I was aware of ๐
I didn't know but i thought something like that might be the case thats why i said django-environ might be worth looking into xD
*Despised
Please tell me thats not how you handle it in all your projects xD
The ones in docker, pretty much...
I wrote this on my phone so it may not be exact
Django-environ, or atleast environ
You've got some library to install xD
Dependencies... count on some meth head in Kentucky to maintain code to not write one line? No thanks ๐
I remember leftpad
(Seriously, I think it's probably fine to use either of those... but meh. I honestly never bothered) (edit: no need for a probably)
It just makes your life easier as it actually can parse dicts, list of emails, database connection strings etc
Its just nice as i can write my docs to say what type of thing that config variable expects and then link to environ's docs on what gets interpreted how
It can also read from a file directly, i could go on
Leftpad was because of the deranged and incredibly stupid js world
The is-even joke was a practical joke in the js world xD
If I worked with a team that uses one of those libs, I would say "ok, cool"...
https://github.com/samuelmarina/is-even
Also with this one?
My env vars are usually fine as simple strings... so I skip the extra deps.
The next version has to have machine learning imo
And a bit eventual consistency, just a tiny bit
If one day I meet a pain point, I'll be sure to add the dependencies ๐
The secret is time. Encounter 10k errors in your lifetime, and you'll solve them in seconds eventually ๐
If it doesn't work, try kicking it 10k times ๐ฆฟ
advocating for violence here
Please don't kick your pc, your cpu can't help it
We shouldn't punish it for something that is not its fault
We're the ones who taught sand to think
I thought the sarcasm was self-evident ๐
Mine wasn't, it seems
Alpine uses it's own package manager. I forget which, as I really only use it for nginx ๐
So you can't use apt or apt-get
You can use the slim variant of Debian to get the best of both worlds...
I usually just use the standard Debian image though.
700 MB or 1 GB is acceptable to me.
Maybe. You need to look at the tags on Docker Hub.
That depends on you and your server ๐
I can afford the 1GB ๐
Because of the way the overlay FS works, you can run multiple containers using the same base image...
If you were to run 10 Django projects (on the same base image), it would still take 1GB plus a few more MB per project, not 10GB.
Shouldn't be...
A good part of the images should be shared.
Docker just takes a base, and applies changes on top.
If they're all derived from the same base image, then a lot of the code only exists as a single copy.
Yeah, you can run docker image inspect hash1 hash2 hash3 using the image IDs, and the RootFS.Layers entries should mostly match with only one layer being different, maybe.
You can also pipe the output to jq for better readability (though you need to install that first).
You can name and tag the image it in the compose file: https://gitlab.com/theepic-dev/theepic.dev/-/blob/main/docker-compose.yml?ref_type=heads#L13
I usually set it to read the git commit tag. If it is set, it tags it with the version, otherwise it defaults to staging.
Though I don't actually have tags in that repo ๐